
import java.io.*;
import java.lang.Runnable;
import java.util.ArrayList;
import java.net.Socket;

class NodoThread implements Runnable{

  private PrintStream err;

  private Socket client;

  private String nodeName;

  private int port;

  private ArrayList<Host> knownHosts;

  private ArrayList<Track> library;

  private Request request;

  private OutputStream os;

  private ObjectOutputStream oos;

  NodoThread(PrintStream err, Socket client, String nodeName, int port, ArrayList<Host> knownHosts, ArrayList<Track> library){
    this.err = err;
    this.client = client;
    this.nodeName = nodeName;
    this.port = port;
    this.knownHosts = knownHosts;
    this.library = library;
  }

  /**
   * Recieves a request and redirects it to the proper handler
   * It also initializes the OutputStream used by handlers
   */
  public void run()
  {

    //Output stream for response
    try{
      os = client.getOutputStream();
    }catch(IOException e){
      err.println("Error obteniendo un OutputStream: " + e.getMessage());
      return;
    }

    //Output stream object for response
    try{
      oos = new ObjectOutputStream(os);
      oos.flush();
    }catch(IOException e){
      err.println("Error obteniendo un objeto de OutputStream: " + e.getMessage());
      return;
    }

    //Input stream for request
    InputStream is;
    try{
      is = client.getInputStream();
    }catch(IOException e){
      err.println("No se pudo obtener el input stream: " + e.getMessage());
      return;
    }

    //Input stream object for request
    ObjectInputStream ois;
    try{
      ois = new ObjectInputStream(is);
    }catch(IOException e){
      err.println("No se pudo obtener el objeto input stream: " + e.getMessage());
      return;
    }

    //Read request object from input stream object
    try{
      request = (Request) ois.readObject();
    }catch(IOException e){
      err.println("No se pudo leer el objeto desde el input stream: " + e.getMessage());
      return;
    }catch(ClassNotFoundException e){
      err.println("La clase para el objeto leído no está cargada: " + e.getMessage());
      return;
    }

    //Close object input stream for request
    //try{
    //  ois.close();
    //}catch(IOException e){
    //  err.println("Error cerrando el objeto de input stream: " + e.getMessage());
    //  return;
    //}

    //Handle request
    try{
      switch (request.getRequestType()){
      case CONSULT:
        handleConsult();
        break;
      case DOWNLOAD:
        handleDownload();
        break;
      case REACHABLE_NODES:
        handleReachableNodes();
        break;
      }
    }catch(NodoThreadException e){
      err.println("Error ejecutando solicitud: " + e.getMessage());
      return;
    }

    //Close socket
    //try{
    //  os.close();
    //  client.close();
    //}catch(IOException e){
    //  err.println("Error cerrando la conexión: " + e.getMessage());
    //  return;
    //}
  }

  /**
   * Performs consult on all known hosts updating the visited hosts list
   * It only sends back one response object with the list of tracks which
   * match the requirements
   */
  private void handleConsult()
    throws NodoThreadException
  {

    ArrayList<Track> consult = new ArrayList<Track>();
    ArrayList<String> visited = request.getVisited();
    //If this node was already visited, then return without doing anything else
    for (String visitedNodeName : visited){
      if (visitedNodeName.equals(nodeName)){
        Response response = new Response();
        response.setConsult(new ArrayList<Track>());
        sendResponse(response);
        return;
      }
    }
    //Right know this is a visited host
    visited.add(nodeName);
    for (Host host : knownHosts){

      try{
        host.startRequest(port);
      }catch(HostException e){
        throw new NodoThreadException("Error iniciando solicitud de consulta: " + e.getMessage());
      }

      //Request additional tracks to this node
      Response response;
      try{
        response = host.request(new Request(request.getCreator(), request.getTitle(), visited, host.getHostName()), err);
      }catch(HostException e){
        throw new NodoThreadException("Error enviando solicitud de consulta: " + e.getMessage());
      }

      try{
        host.endRequest();
      }catch(HostException e){
        throw new NodoThreadException("Error finalizando solicitud de consulta: " + e.getMessage());
      }

      //Join results
      for (Track t : response.getConsult()){
        //Check if this song is already in the result set
        boolean found = false;
        for (Track te : consult){
          if (t.equals(te)){
            found = true;
          }
        }
        //Song already in the result set
        if (found){
          continue;
        }
        consult.add(t);
      }
    }
    //Add local results
    for (Track t : library){
      if (-1 != t.getCreator().toLowerCase().indexOf(request.getCreator())
       && -1 != t.getTitle().toLowerCase().indexOf(request.getTitle())){
        //Each client will reach us with some ip. Return the ip on each consult
        t.setServerName(request.getServerName());
        consult.add(t);
      }
    }

    Response response = new Response();
    response.setConsult(consult);
    sendResponse(response);
  }

  /**
   * This handles a reachable nodes request. When it recursively finds all
   * reachable nodes, returns only one response with this list
   */
  private void handleReachableNodes()
    throws NodoThreadException
  {

    ArrayList<String> nodes = request.getVisited();
    //If this node was already visited, then return without doing anything else
    for (String visitedNodeName : nodes){
      if (visitedNodeName.equals(nodeName)){
        Response response = new Response();
        response.setReachableNodes(new ArrayList<String>());
        sendResponse(response);
        return;
      }
    }
    //Right know this is a known host
    nodes.add(nodeName);
    for (Host h : knownHosts){
 
      try{
        h.startRequest(port);
      }catch(HostException e){
        throw new NodoThreadException("Error iniciando solicitud de nodos alcanzables: " + e.getMessage());
      }

      //Request additional tracks to this node
      Response response;
      try{
        response = h.request(new Request(nodes));
      }catch(HostException e){
        throw new NodoThreadException("Error enviando solicitud de nodos alcanzables: " + e.getMessage());
      }

      try{
        h.endRequest();
      }catch(HostException e){
        throw new NodoThreadException("Error finalizando solicitud de nodos alcanzables: " + e.getMessage());
      }

      //Join lists
      for (String newNodeName : response.getReachableNodes()){
        boolean inList = false;
        for (String knownNodeName : nodes){
          if (newNodeName.equals(knownNodeName)){
            inList = true;
          }
        }
        if (!inList){
          nodes.add(newNodeName);
        }
      }
    }

    Response response = new Response();
    response.setReachableNodes(nodes);
    sendResponse(response);
  }

  /**
   * Handles a download. First it makes sure the file exists and returns
   * a response object through the stream. If the file is valid it also
   * returns the file through the stream
   */
  private void handleDownload() throws NodoThreadException{
    File file;
    Response response = new Response();
    try{
        file = new File(request.getLocation());
        response.setSize(file.exists() ? file.length() : -1);
        //Send response to let the user know if it must handle a download
        this.sendResponse(response);
        if(!file.exists())
            return;
        
        BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
        //This creates a buffer with 1MB, this ensures that the ammount of bytes passed
        // at a time to the stream will be of a valid size, but also guarantees efficient
        // transfer speed
        byte buffer[] = new byte[1048576];
        //Used to determine how many bytes are available in the file
        long available;

        while((available = in.available()) != 0){
            int toWrite = (int) Math.min(1048576, available);
            in.read(buffer, 0, toWrite);
            os.write(buffer, 0, toWrite);
        }
    }catch(IOException ex){
        throw new NodoThreadException("IOException: " + ex.toString());
    }
  }

  /**
   * Generic reponse return from a handler. It dose so through the OutputStream
   */
  private void sendResponse(Response response){

    //Write response
    try{
      oos.writeObject(response);
    }catch(IOException e){
      err.println("Error escribiendo la respuesta: " + e.getMessage());
      return;
    }

    //Close socket
    //try{
    //  oos.close();
    //}catch(IOException e){
    //  err.println("Error cerrando la conexión: " + e.getMessage());
    //  return;
    //}  
  }
}